/**
 * 功能： LED智能灯控制系统 设备端1
 * 作者： 单片机菜鸟哥
 * 时间： 2020-05-02
 * 通信方式：MQTT客户端直连 + Web配网，通过小程序控制
 */

#include <ESP8266WiFi.h>
#include <ArduinoJson.h>
#include <WiFiClientSecure.h>
#include <ESP8266WebServer.h>
#include <PubSubClient.h>

//for LED status
#include <Ticker.h>
#include <EEPROM.h>
#include <CustomWiFiManager.h>         // https://github.com/tzapu/WiFiManager
#include "utils_hmac.h"
#include <OneButton.h>

#define DEBUG //是否开启debug功能

#ifdef DEBUG
#define DebugBegin(b)    Serial.begin(b)
#define DebugPrintln(message)    Serial.println(message)
#define DebugPrint(message)    Serial.print(message)
#else
#define DebugBegin(b)
#define DebugPrintln(message)
#define DebugPrint(message)
#endif

const char yellowWifi[] PROGMEM = "";

/******************产品/设备配置(每个人需要根据自己的产品设备信息去动态更换)**********************/
#define REGION_ID   "cn-shanghai"  //地域
#define PRODUCT_KEY "a1BerytISTi"  //产品key  从产品详情获取
// 服务端相关
#define MQTT_SERVER   PRODUCT_KEY ".iot-as-mqtt." REGION_ID ".aliyuncs.com" //阿里云MQTT服务地址
#define MQTT_PORT     1883                                               //MQTT服务端口
#define HTTPS_SERVER "iot-auth." REGION_ID ".aliyuncs.com" //认证地址
#define HTTPS_PORT 443

// 相关主题
#define TOPIC_SET_PROPERTY "/sys/" PRODUCT_KEY "/{dName}/thing/service/property/set"
/*************************************************************************************************/

static char MQTT_CLIENT_ID[50]; // Mqtt ClientID
static char MQTT_USERNAME[40];  // Mqtt UserName
static char MQTT_PASSWORD[50];  // Mqtt Psw
static char MQTT_TOPIC_STATUS[100];

#define CONFIG_NUMBER 0xAA
#define NOCONFIG_NUMBER 0x00

void initWifiManager();
void saveConfigCallback();
void configModeCallback (WiFiManager *myWiFiManager);
void doWiFiTick();
void connectToMqtt();
bool connectHTTPS();
bool loadROOTcert();
void deviceAuthen();
void sendHTTPSrequest();
void readHTTPSreponse();
void mqtt_callback (char* topic, byte* payload, unsigned int length);
void parseMqttResponse(char* payload);
String charToString(char *src,int len);
void onButtonClick();

// 结构体 存储到eeprom
struct device_config{
  char deviceName[40];  //设备deviceName  从设备详情获取
  char deviceSecret[40];  //对应的设备秘钥
  char ssid[20];
  char psw[20];
  uint8_t magic;
} config;

// https
WiFiClientSecure client_s;
// mqtt
PubSubClient mqttclient(MQTT_SERVER, MQTT_PORT, &mqtt_callback, client_s);

//flag for saving data
bool shouldSaveConfig = false;
int state = 0;
//for LED status
Ticker ticker;
#define buttonPin D4
OneButton button(buttonPin, true);

void setup() {
  // put your setup code here, to run once:
  delay(2000);
  DebugBegin(115200);
  pinMode(LED_BUILTIN, OUTPUT);
  button.attachDoubleClick(onButtonClick);
  digitalWrite(LED_BUILTIN, state);
  WiFi.disconnect();
  loadConfig();
  initWifiManager();
  client_s.setInsecure();
}

void loop() {
  // put your main code here, to run repeatedly:
  ESP.wdtFeed();
  button.tick();
  doWiFiTick();
  if (WiFi.status() == WL_CONNECTED) {
      connectToMqtt();
      mqttclient.loop();
    }
}

/**
 * 功能描述：初始化wifimanager
 */
void initWifiManager(){
  /***  步骤一：创建 wifimanager对象 **/
  WiFiManager wifiManager;
  /*************************************/
  /*** 步骤二：进行一系列配置，参考配置类方法 **/
  // 打印调试内容
  #ifdef DEBUG
     wifiManager.setDebugOutput(true);
  #endif
  // 设置个人图标  大叔黄
  wifiManager.setHeadImgBase64(FPSTR(yellowWifi));
  wifiManager.setButtonBackground("#E08E00");
  // 设置最小信号强度
  wifiManager.setMinimumSignalQuality(40);
  // 设置固定AP信息
  IPAddress _ip = IPAddress(192, 168, 4, 1);
  IPAddress _gw = IPAddress(192, 168, 4, 1);
  IPAddress _sn = IPAddress(255, 255, 255, 0);
  wifiManager.setAPStaticIPConfig(_ip, _gw, _sn);
  // 设置进入AP模式的回调
  wifiManager.setAPCallback(configModeCallback);
  wifiManager.setConnectTimeout(5);
  // 设置点击保存的回调
  wifiManager.setSaveConfigCallback(saveConfigCallback);
  // 设置 如果配置错误的ssid或者密码 退出配置模式
  wifiManager.setBreakAfterConfig(true);
  // 添加额外的参数 设备名字 设备秘钥
  WiFiManagerParameter custom_device_name("dn", "DeviceName", config.deviceName, 40);
  WiFiManagerParameter custom_device_secret("ds", "DeviceSecret", config.deviceSecret, 40);
  if (config.magic == CONFIG_NUMBER){
    wifiManager.setSSID(config.ssid);
    wifiManager.setPassword(config.psw);
  }

  wifiManager.addParameter(&custom_device_name);
  wifiManager.addParameter(&custom_device_secret);

  /*************************************/
  /*** 步骤三：尝试连接网络，失败去到配置页面 **/
  String mac = WiFi.macAddress();
  mac.replace(":","");
  String ssid = String("ESP") + "-" + mac;
  if (!wifiManager.autoConnect(ssid.c_str(),"12345678")) {
      DebugPrintln("failed to connect and hit timeout");
      //reset and try again, or maybe put it to deep sleep
      ESP.reset();
      delay(1000);
  }
  /*************************************/
  // 保存自定义信息
  if (shouldSaveConfig) {
      // 读取配置页面配置好的信息
      strcpy(config.deviceName, custom_device_name.getValue());
      strcpy(config.deviceSecret, custom_device_secret.getValue());
      strcpy(config.ssid, wifiManager.getSSID().c_str());
      strcpy(config.psw, wifiManager.getPassword().c_str());

      saveConfig();

      #ifdef DEBUG
         DynamicJsonBuffer jsonBuffer;
         JsonObject& json = jsonBuffer.createObject();
         json["ssid"] = config.ssid;
         json["psw"] = config.psw;
         json["device_name"] = config.deviceName;
         json["device_secret"] = config.deviceSecret;
         json.printTo(Serial);
         DebugPrintln("");
      #endif
  }

  String topicSetProperty = TOPIC_SET_PROPERTY;
  // deviceName placeholder替换
  topicSetProperty.replace("{dName}",config.deviceName);
  strcpy(TOPIC_SET_PROPERTY, topicSetProperty.c_str());

  DebugPrint(F("topicSetProperty:"));
  DebugPrintln(topicSetProperty);

  DebugPrintln(F("local ip"));
  DebugPrintln(WiFi.localIP());
  // 我们在loop里面去做检测
  WiFi.disconnect();
  ticker.detach();
}

/**
 * 功能描述：配置进入AP模式通知回调
 */
void configModeCallback (WiFiManager *myWiFiManager) {
  DebugPrintln(F("Entered config mode"));
  DebugPrintln(WiFi.softAPIP());
  // if you used auto generated SSID, print it
  DebugPrintln(myWiFiManager->getConfigPortalSSID());
  // entered config mode, make led toggle faster
  ticker.attach(0.2, tick);
}

/**
 * 功能描述：设置点击保存的回调
 */
void saveConfigCallback () {
  DebugPrintln(F("Should save config"));
  shouldSaveConfig = true;
}

/**
 * 功能描述：设置LED灯闪烁，提示用户进入配置模式
 */
void tick(){
  //toggle state
  int state = digitalRead(BUILTIN_LED);  // get the current state of GPIO1 pin
  digitalWrite(BUILTIN_LED, !state);     // set pin to the opposite state
}

/**
 * 功能：连接路由心跳函数
 */
void doWiFiTick() {
  static bool taskStarted = false;
  static bool startSTAFlag = false;
  static uint32_t lastWiFiCheckTick = 0;

  if (!startSTAFlag) {
    startSTAFlag = true;

    DebugPrint("connect to ap:");
    DebugPrintln(config.ssid);
    WiFi.disconnect();
    WiFi.mode(WIFI_STA);
    WiFi.begin(config.ssid, config.psw);
  }

  //未连接1s重连
  if ( WiFi.status() != WL_CONNECTED ) {
    if (millis() - lastWiFiCheckTick > 1000) {
      lastWiFiCheckTick = millis();
      DebugPrint(".");
    }
  }
  //连接成功建立
  else {
    if (taskStarted == false) {
      taskStarted = true;
      DebugPrint("\r\nGet IP Address: ");
      DebugPrintln(WiFi.localIP());
      deviceAuthen();
    }
  }
}

/**
 * 生成 HmacSha1
 * @param sign HmacSha1字符串
 */
static ICACHE_FLASH_ATTR char* parseHmacSha1(char *sign) {//
  String chipidStr = String(ESP.getChipId(), HEX);//"ef63aa";
  String clientIDStr = String(PRODUCT_KEY) + "." + chipidStr;
  char *hmac_source;//[120];
  strcpy(MQTT_CLIENT_ID, clientIDStr.c_str());
  String str = "clientId" + clientIDStr + "deviceName" + config.deviceName + "productKey" + PRODUCT_KEY;
  uint8_t str_len = str.length() + 1;
  
  hmac_source = (char*)malloc(str_len * sizeof(char));
  memcpy(hmac_source, str.c_str(), str_len);

  DebugPrint(F("hmac_source: "));
  DebugPrintln(hmac_source);

  utils_hmac_sha1(hmac_source, strlen(hmac_source), sign, config.deviceSecret, strlen(config.deviceSecret));

  DebugPrint(F("signature: "));
  DebugPrintln(sign);

  free(hmac_source);
  return sign;
}

/***
 * 发送验证请求
 */
void ICACHE_FLASH_ATTR sendHTTPSrequest() {
  DebugPrintln(F("sendHTTPSrequest"));
  char signature[40];
  parseHmacSha1(signature);
  String signatureStr = charToString(signature,40);
  DebugPrint(F("signatureStr: "));
  DebugPrintln(signatureStr);

  String str = String("productKey=") + PRODUCT_KEY + "&deviceName=" + config.deviceName + "&signmethod=hmacsha1&sign=" + signatureStr +
               "&version=default&clientId=" + MQTT_CLIENT_ID + "&resources=mqtt";

  String str1 = (String("POST ") + "/auth/devicename HTTP/1.1\r\nHost: " + HTTPS_SERVER + "\r\n" +
                 "Accept: text/xml,text/javascript,text/html,application/json\r\nContent-Type: application/x-www-form-urlencoded\r\n" +
                 "Content-Length: " + str.length() + "\r\n\r\n");
  client_s.print(str1);
  DebugPrint(str1);

  client_s.print(str);
  DebugPrintln(str);

  DebugPrint(F("request sent!"));
  delay(100);
}

/**
 * 处理认证请求
 */
void ICACHE_FLASH_ATTR readHTTPSreponse() {
  DebugPrintln(F("readHTTPSreponse"));
  while (client_s.connected())   {
    String line = client_s.readStringUntil('\n');
    if (line == "\r") {
      DebugPrintln(F("=========="));
      break;
    }
  }
  String resstring = client_s.readStringUntil('\n');

  DebugPrint(F("receiving: "));
  DebugPrintln(resstring);

  DynamicJsonBuffer jsonBuffer;
  JsonObject& root = jsonBuffer.parseObject(resstring);

  // Test if parsing succeeds.
  if (!root.success()) {
     DebugPrintln("parse failed");
     return;
  } else {
     #ifdef DEBUG
          // 格式化打印json
          root.prettyPrintTo(Serial);
     #endif
     DebugPrintln(F("parse success"));
  }

  strcpy(MQTT_USERNAME, root["data"]["iotId"]);
  strcpy(MQTT_PASSWORD, root["data"]["iotToken"]);

  DebugPrint(F("MQTT_CLIENT_ID: "));
  DebugPrintln(MQTT_CLIENT_ID);
  DebugPrint(F("MQTT_USERNAME: "));
  DebugPrintln(MQTT_USERNAME);
  DebugPrint(F("MQTT_PASSWORD: "));
  DebugPrintln(MQTT_PASSWORD);

  DebugPrintln(F("=========="));
  client_s.stop();
}

/*
 * 设备认证
 */
void deviceAuthen() {
  while (loadROOTcert()) {
    while (connectHTTPS()) {
      sendHTTPSrequest();
      readHTTPSreponse();
      return;
    }
    ESP.wdtFeed();
  }
}

/*
 * 连接到远程服务器并且获取状态
 */
bool ICACHE_FLASH_ATTR connectHTTPS() {
  DebugPrint(F("connecting to "));
  DebugPrintln(HTTPS_SERVER);
  if (!client_s.connect(HTTPS_SERVER, HTTPS_PORT)) {
    DebugPrintln(F("connection failed"));
    return false;
  }else {
    DebugPrintln(F("connection succeed"));
    return true;
  }
}

/*
 * 设置根证书并且返回状态
 */
bool ICACHE_FLASH_ATTR loadROOTcert() {
  return true;
}

void connectToMqtt() {
    if (mqttclient.connected()) {
     return;
    }
    DebugPrint(F("Connecting to MQTT... "));
    int8_t ret;
    uint8_t retries = 3;

    while (!mqttclient.connect(MQTT_CLIENT_ID, MQTT_USERNAME,MQTT_PASSWORD)) { // connect will return true for connected
          DebugPrintln(F("Retrying MQTT connection in 5 seconds..."));
          mqttclient.disconnect();
          delay(5000);  // wait 5 seconds
          retries--;
          if (retries == 0) {
            // basically die and wait for WDT to reset me
            while (1);
          }
          yield();
    }
    DebugPrint(F("Connect MQTT Success!"));
    // 订阅主题
    mqttclient.subscribe(TOPIC_SET_PROPERTY);
}

/**
 * 解析mqtt数据
 */
void parseMqttResponse(char* payload){
   DebugPrintln(F("parseMqttResponse"));
   StaticJsonBuffer<300> jsonBuffer;
     // StaticJsonBuffer 在栈区分配内存   它也可以被 DynamicJsonBuffer（内存在堆区分配） 代替
     // DynamicJsonBuffer  jsonBuffer;
   JsonObject& root = jsonBuffer.parseObject(payload);
     // Test if parsing succeeds.
   if (!root.success()) {
       DebugPrintln(F("parse failed"));
       return ;
   } else {

       #ifdef DEBUG
          // 格式化打印json
          root.prettyPrintTo(Serial);
       #endif

       DebugPrintln(F("parse success"));
   }

   if (root.containsKey("params")){
        JsonObject &params = root["params"];
        if (params.containsKey("status")) {
            state = !state;
            digitalWrite(LED_BUILTIN, state);
        }
   }
}

/*
 * 保存参数到EEPROM
*/
void saveConfig(){
  DebugPrintln(F("-----Save config To EEPROM!-----"));
  EEPROM.begin(300);
  config.magic = CONFIG_NUMBER;
  uint8_t *p = (uint8_t*)(&config);
  for (int i = 0; i < sizeof(config); i++){
    EEPROM.write(i, *(p + i));
  }
  EEPROM.commit();
  DebugPrintln(F("---------   Save End   ----------"));
}

/*
 * 从EEPROM加载参数
*/
void loadConfig(){
  DebugPrintln(F("-----Read config From EEPROM!-----"));
  EEPROM.begin(300);
  uint8_t *p = (uint8_t*)(&config);
  for (int i = 0; i < sizeof(config); i++){
    *(p + i) = EEPROM.read(i);
  }
  EEPROM.commit();

  if (config.magic == CONFIG_NUMBER){
      DebugPrint(F("deviceName:"));
      DebugPrintln(config.deviceName);
      DebugPrint(F("deviceSecret:"));
      DebugPrintln(config.deviceSecret);
      DebugPrint(F("ssid:"));
      DebugPrintln(config.ssid);
      DebugPrint(F("psw:"));
      DebugPrintln(config.psw);
  } else{
    DebugPrintln(F("Has No config"));
    config.magic == NOCONFIG_NUMBER;
    memset(config.deviceName, 0, 40);
    memset(config.deviceSecret, 0, 40);
    memset(config.ssid, 0, 40);
    memset(config.psw, 0, 40);
  }
  DebugPrintln(F("-------------  Read End  --------------"));
}

/**
 * 数组转字符串
 */
String charToString(char *src,int len){
   String str = "";
   for (byte i = 0 ; i < len; i++) {
       str += src[i];
   }
   return str;
}

// 重置
void onButtonClick() {
  DebugPrintln(F("Button  click."));
  config.magic = NOCONFIG_NUMBER;
  memset(config.deviceName, 0, 40);
  memset(config.deviceSecret, 0, 40);
  memset(config.ssid, 0, 40);
  memset(config.psw, 0, 40);
  saveConfig();
  ESP.reset();
  delay(1000);
} // click1


/**
 * 功能：MQTT回调
 * 参数：
 *   1. topic   主题
 *   2. payload 载体
 *   3. length  载体长度
 */
void mqtt_callback (char* topic, byte* payload, unsigned int length) {
  parseMqttResponse((char *)payload);
}
